Skip to content

refactor(lint): migrate from ESLint + Prettier to oxlint + oxfmt#174

Merged
AnnatarHe merged 1 commit intomasterfrom
refactor/migrate-eslint-prettier-to-oxlint-oxfmt
Mar 21, 2026
Merged

refactor(lint): migrate from ESLint + Prettier to oxlint + oxfmt#174
AnnatarHe merged 1 commit intomasterfrom
refactor/migrate-eslint-prettier-to-oxlint-oxfmt

Conversation

@AnnatarHe
Copy link
Member

@AnnatarHe AnnatarHe commented Mar 21, 2026

Summary

  • Replace ESLint (v10) and Prettier (v3.8) with oxlint and oxfmt from the Oxc project
  • ~180ms lint, ~490ms format across 400+ files (massive speed improvement)
  • Removed 176 packages (eslint, eslint-config-next, prettier, and all plugins)
  • Built-in import sorting and Tailwind CSS class sorting (no plugins needed)
  • Fixed all lint warnings: unused imports, missing alt/lang attrs, iframe sandbox

Test plan

  • pnpm lint — 0 warnings, 0 errors
  • pnpm format:check — all files correctly formatted
  • pnpm test — all 7 tests pass
  • pnpm build — verify production build succeeds

🤖 Generated with Claude Code


Open with Devin

Replace ESLint (v10) and Prettier (v3.8) with oxlint and oxfmt from the
Oxc project for significantly faster linting and formatting (~180ms lint,
~490ms format across 400+ files).

- Remove eslint, eslint-config-next, prettier, and prettier plugins
- Add oxlint and oxfmt with equivalent rule configurations
- Built-in import sorting and Tailwind class sorting (no plugins needed)
- Fix all lint warnings: unused imports, missing alt/lang attrs, iframe sandbox
- Update package.json scripts, lint-staged, and lefthook pre-commit hook
- Update CLAUDE.md documentation references

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@AnnatarHe AnnatarHe merged commit c073a18 into master Mar 21, 2026
2 checks passed
@AnnatarHe AnnatarHe deleted the refactor/migrate-eslint-prettier-to-oxlint-oxfmt branch March 21, 2026 15:39
@claude
Copy link
Contributor

claude bot commented Mar 21, 2026

Code Review: refactor(lint) — ESLint + Prettier → oxlint + oxfmt

This is a well-motivated migration. The tooling switch is clean, the scope is clearly bounded (formatting-only changes across source files), and the speed gains are real. A few things worth noting for follow-up:


Bugs / Issues

package.json uses npm run in scripts (lines 8 & 15)

"build": "npm run codegen && next build --turbo",
"test:update": "npm test -- --updateSnapshot",

CLAUDE.md explicitly states: "This project uses pnpm exclusively. Do not use npm or yarn." These should be pnpm run codegen and pnpm test -- --updateSnapshot. In a pnpm workspace, calling npm run may produce unexpected results or fail outright in strict environments.


Pre-commit Hook Missing Formatter

lefthook.yml only runs the linter on commit — oxfmt is never invoked. The lint-staged config in package.json does include both tools, but since the project uses lefthook (not husky + lint-staged for actual hook execution), formatting is not enforced at commit time. Files can be committed without running the formatter, which will cause pnpm format:check to fail in CI.

Suggestion — add an oxfmt command to lefthook.yml:

pre-commit:
  parallel: true
  commands:
    oxlint:
      glob: "*.{js,ts,jsx,tsx}"
      run: pnpm run lint:fix {staged_files}
    oxfmt:
      glob: "*.{js,ts,tsx,css,md,json}"
      run: pnpm run format {staged_files}

Dead Code: Husky Config in package.json

The project uses lefthook, not husky, but the old husky block is still present in package.json. This has no effect and can be removed to avoid confusion.


Unchecked Build Verification

The test plan has pnpm build unchecked. Given that 400+ files were reformatted with import sorting enabled, the build should be verified to confirm no import paths were silently reordered in a way that affects behavior.


Disabled Rules Worth Revisiting (follow-up)

Rule Risk
react-hooks/exhaustive-deps: "off" Stale closures and subtle bugs in hooks
@typescript-eslint/no-explicit-any: "off" Gradual erosion of type safety
@next/next/no-img-element: "off" May miss <img> tags that should use Next.js <Image>

Not blockers for this PR — but enabling them in a follow-up with a targeted fix pass would improve code quality.


Minor: Local $schema Path in .oxfmtrc.json

The schema path "./node_modules/oxfmt/configuration_schema.json" works locally but silently fails in editors without node_modules installed. An upstream hosted URL would be more portable for IDE schema validation.


Overall: Good cleanup — removing 176 packages and getting sub-200ms lint times is a meaningful win. The main actionable follow-ups are fixing npm run to pnpm run in scripts and adding oxfmt to the lefthook pre-commit hook so formatting is enforced locally.

@gemini-code-assist
Copy link

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request overhauls the project's code quality infrastructure by adopting Oxc's oxlint and oxfmt. The primary goal is to enhance developer experience through faster feedback loops and a more efficient build process, while also reducing the project's overall footprint by consolidating tooling.

Highlights

  • Linting and Formatting Migration: The project has migrated from using ESLint and Prettier to Oxc's oxlint and oxfmt tools for linting and code formatting.
  • Performance Improvement: This migration results in a significant speed improvement, with linting taking ~180ms and formatting ~490ms across over 400 files.
  • Dependency Reduction: The change led to the removal of 176 packages related to ESLint, Prettier, and their plugins, streamlining the project's dependencies.
  • Enhanced Code Quality: The new setup includes built-in import sorting and Tailwind CSS class sorting without needing additional plugins, and all previous lint warnings (unused imports, missing alt/lang attrs, iframe sandbox) have been fixed.
  • Configuration Updates: New configuration files (.oxfmtrc.json and .oxlintrc.json) have been added, and existing development scripts and pre-commit hooks (package.json, lefthook.yml, CLAUDE.md) have been updated to reflect the new tools.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review


return (
<html
lang="en"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Hardcoded lang="en" prevents CJK font CSS rules from matching

The PR adds a hardcoded lang="en" to the <html> element, but the app supports English, Chinese, Japanese, and Korean. The CSS in src/styles/global.css:117-127 contains language-specific font rules (html[lang='zh'], html[lang='ja'], html[lang='ko']) that apply the LxgwWenKai font for CJK languages. With lang hardcoded to "en", these rules will never match, so CJK users won't get the proper font at the page level. The i18n client config at src/i18n/client.ts:43 only uses ['cookie'] detection order (the htmlTag detector is commented out), so i18next will not dynamically update the lang attribute. The lang should be dynamic based on the user's selected language.

Prompt for agents
In src/app/layout.tsx, the lang attribute on the <html> element at line 86 is hardcoded to "en". This needs to be dynamic based on the user's language preference. Since this is a server component, you should read the language cookie (STORAGE_LANG_KEY = 'ck-lang') from next/headers cookies() and use it to set the lang attribute. Fallback to 'en' if no cookie is present. For example:

import { cookies } from 'next/headers'
import { STORAGE_LANG_KEY } from '@/constants/storage'

Then inside the Layout function:
const cookieStore = await cookies()
const lang = cookieStore.get(STORAGE_LANG_KEY)?.value || 'en'

And use lang={lang} on the <html> element instead of lang="en".

This ensures that the CSS font rules in src/styles/global.css (html[lang='zh'], html[lang='ja'], html[lang='ko']) will properly match and apply the LxgwWenKai font for CJK users.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

width='1024px'
title="guide"
src="//player.bilibili.com/player.html?aid=503430935&bvid=BV1Tg411G7gG&cid=349347987&page=1"
sandbox="allow-scripts allow-popups"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 iframe sandbox without allow-same-origin breaks embedded bilibili video player

The PR adds sandbox="allow-scripts allow-popups" to bilibili video player iframes in two export settings pages. Without allow-same-origin, the sandboxed iframe content is treated as being from a unique opaque origin, preventing it from accessing cookies, localStorage, and making same-origin requests to bilibili's servers. This will cause the video player to fail to load or play content, as video players typically need these capabilities for authentication, content delivery, and playback. The sandbox attribute was not present before this PR.

Affected files
  • src/app/dash/[userid]/settings/exports/export.email.tsx:82
  • src/app/dash/[userid]/settings/exports/export.notion.tsx:106
Suggested change
sandbox="allow-scripts allow-popups"
sandbox="allow-scripts allow-popups allow-same-origin"
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

)}
onClick={() => props.onSelecte(book)}
aria-selected={selected}
data-selected={selected}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 aria-selected replaced with non-semantic data-selected, removing accessibility state

The aria-selected={selected} attribute on the book candidate <li> element was changed to data-selected={selected}. This removes the ARIA state that communicates the selected/deselected status of list items to assistive technologies (screen readers). Users relying on screen readers will no longer be informed which book candidate is currently selected. The data-selected attribute has no accessibility semantics.

Suggested change
data-selected={selected}
aria-selected={selected}
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request is a great initiative to modernize the linting and formatting setup by migrating from ESLint and Prettier to the much faster Oxc toolchain (oxlint and oxfmt). The changes are extensive and include updating configuration files, package dependencies, and scripts, as well as applying the new formatting across the codebase. The author also took the opportunity to fix several existing lint issues, such as missing lang and sandbox attributes, which improves accessibility and security.

My review focuses on ensuring the new configuration is robust and effective. I've pointed out a few areas for improvement:

  • The new oxlint configuration disables several important rules, particularly react-hooks/exhaustive-deps and some accessibility rules. I've recommended re-enabling these to prevent potential bugs and improve accessibility.
  • The pre-commit hook configuration in lefthook.yml seems to have a bug that would cause it to lint the entire project on every commit, instead of just the staged files. I've suggested a fix for this.
  • There appears to be a redundant lint-staged configuration in package.json which could be removed to simplify the setup.

Overall, this is a valuable refactoring. Addressing these configuration points will help fully leverage the benefits of the new tooling.

Comment on lines +11 to +30
"rules": {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-non-null-assertion": "off",
"react/react-in-jsx-scope": "off",
"react/no-unescaped-entities": "off",
"react-hooks/exhaustive-deps": "off",
"@next/next/no-img-element": "off",
"@next/next/no-html-link-for-pages": "off",
"prefer-template": "warn",
"no-useless-escape": "warn",
"import/no-unassigned-import": "off",
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "off",
"no-shadow": "off",
"react/no-array-index-key": "off",
"jsx-a11y/no-static-element-interactions": "off",
"jsx-a11y/click-events-have-key-events": "off",
"no-await-in-loop": "off"
},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Disabling important linting rules like react-hooks/exhaustive-deps and accessibility rules (jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events) can introduce subtle bugs and accessibility issues. While these might have been disabled before, this migration is a good opportunity to enable them (at least as warnings) and address the violations. This would significantly improve the application's correctness and accessibility. For cases where a rule is too noisy, you can disable it for a specific line with an explanatory comment.

oxlint:
glob: "*.{js,ts,jsx,tsx}"
run: npm run lint -- --fix {staged_files}
run: pnpm run lint:fix {staged_files}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current command pnpm run lint:fix {staged_files} will execute oxlint --fix src/ and ignore the {staged_files} placeholder, because pnpm doesn't pass arguments to scripts without --. This means the pre-commit hook will lint and fix the entire src/ directory, not just the staged files, which is inefficient and likely not the intended behavior. To fix this and lint only staged files, you should call oxlint directly.

      run: pnpm oxlint --fix {staged_files}

Comment on lines 192 to 202
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix",
"prettier --write",
"oxlint --fix",
"oxfmt",
"git add"
],
"*.{css,md,json}": [
"prettier --write",
"oxfmt",
"git add"
]
},

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It seems lefthook is being used for pre-commit hooks, with commands defined in lefthook.yml. This lint-staged configuration appears to be redundant and might not be executed. To avoid confusion and keep the configuration clean, consider either removing this lint-staged section or configuring lefthook to run lint-staged.

width='1024px'
title="guide"
src="//player.bilibili.com/player.html?aid=503430935&bvid=BV1Tg411G7gG&cid=349347987&page=1"
sandbox="allow-scripts allow-popups"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Adding the sandbox attribute to iframes is a crucial security measure that restricts the content within the iframe from performing potentially malicious actions. However, the current implementation sandbox="allow-scripts allow-popups" is still quite permissive. For embedding content from a trusted source like Bilibili, this might be acceptable, but it's worth evaluating if more restrictions can be applied. For instance, if the embedded player doesn't need to open popups, you could remove allow-popups. The principle of least privilege should be applied.

width='1024px'
title="guide"
src="//player.bilibili.com/player.html?aid=503430935&bvid=BV1Tg411G7gG&cid=349347987&page=1"
sandbox="allow-scripts allow-popups"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Adding the sandbox attribute to iframes is a crucial security measure that restricts the content within the iframe from performing potentially malicious actions. However, the current implementation sandbox="allow-scripts allow-popups" is still quite permissive. For embedding content from a trusted source like Bilibili, this might be acceptable, but it's worth evaluating if more restrictions can be applied. For instance, if the embedded player doesn't need to open popups, you could remove allow-popups. The principle of least privilege should be applied.


return (
<html
lang="en"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Adding lang="en" to the <html> tag is a great improvement for accessibility and SEO. However, since the application supports multiple languages (as seen from the i18n setup), this should be set dynamically based on the user's selected language. Hardcoding it to en will be incorrect for users browsing in other languages.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant